Skip to content

feat(data): unarchive() primitive on ConversationRepository (#96)#112

Merged
ilmoniemi merged 3 commits into
mainfrom
feature/96
May 14, 2026
Merged

feat(data): unarchive() primitive on ConversationRepository (#96)#112
ilmoniemi merged 3 commits into
mainfrom
feature/96

Conversation

@ilmoniemi
Copy link
Copy Markdown
Contributor

What

Adds suspend fun unarchive(conversationId: String) to ConversationRepository and implements it in FakeConversationRepository as the inverse of archive(). Clears the archived flag so the conversation reappears in the Discussions (or Channels) stream and leaves Archived.

Issue

Closes #96

Architecture compliance

Follows the spec at docs/specs/architecture/96-unarchive-primitive.md exactly:

  • Interface method placed immediately below archive() (line 32) — inverse pairing visible at a glance.
  • Fake impl mirrors archive() structure (atomic state.update, throw unknown(id) for unknown IDs, copy(archived = false)).
  • Idempotent on already-unarchived conversations: copy(archived = false) produces an equal Conversation value; StateFlow deduplicates equal emissions. Matches the established archive() convention.
  • No domain-model change (Conversation.archived already exists from feat(data): retain conversations on archive() + expose archived filter + seed #93).

Testing

Three new unit tests in FakeConversationRepositoryTest, mirroring the existing archive_* cluster:

  • unarchive_movesConversation_from_Archived_to_Discussions_andRetainsInStore — the AC test, using the existing seed-discussion-archived seed.
  • unarchive_onUnknownId_throwsIllegalArgumentException on unknown ID.
  • unarchive_isIdempotent — two consecutive calls leave the conversation in Discussions exactly once.

TDD: RED first (compile failure on unresolved unarchive), then GREEN after adding interface + impl.

Test fakes in ChannelListViewModelTest and DiscussionListViewModelTest got matching TODO("not used") stubs to satisfy the new interface member — same pattern used for archive() in those files.

Build status

  • ./gradlew :app:testDebugUnitTest — pass
  • ./gradlew :app:lint — pass (no new findings)
  • ./gradlew :app:assembleDebug — pass
  • ./gradlew :app:connectedAndroidTest — not run (no device/emulator connected in dispatcher env); change is data-layer only, no UI surface.

🤖 Generated with Claude Code

ilmoniemi and others added 2 commits May 14, 2026 09:23
Adds the inverse of archive() so the Archived Discussions UI (#94) can
restore an archived discussion. The Fake implementation clears the
archived flag; behavior mirrors archive() (unknown ID → IllegalArgumentException,
idempotent on already-unarchived conversations).

Closes #96

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ilmoniemi
Copy link
Copy Markdown
Contributor Author

Code Review: #96

Decision: PASS

Findings

None.

Summary

Trivial inverse of archive(). Implementation mirrors the existing function structurally line-for-line: atomic state.update { }, throw unknown(id) on missing, copy(archived = false). Idempotent semantics fall out of StateFlow deduplication of equal emissions — matching the established convention locked in by archive_isIdempotent. No KDoc bloat, no over-engineering.

Interface change is purely additive. All 6 ConversationRepository implementations got matching TODO("not used") stubs (1 in FakeConversationRepository, 2 in ChannelListViewModelTest, 4 in DiscussionListViewModelTest) — no missed call sites.

Tests follow the archive_* cluster idiom precisely:

  • unarchive_movesConversation_from_Archived_to_Discussions_andRetainsInStore exercises the AC end-to-end (uses the existing seed-discussion-archived seed; pre-asserts initial state; post-asserts movement across all 4 filters including the negative on Channels).
  • unarchive_onUnknownId_throws mirrors archive_onUnknownId_throws.
  • unarchive_isIdempotent mirrors archive_isIdempotent.

Spec doc at docs/specs/architecture/96-unarchive-primitive.md matches what shipped — no drift between architect intent and implementation.

No security-sensitive label and no ## Design source section in the spec, so security and visual fidelity passes do not apply (data-layer only, non-visual).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ilmoniemi ilmoniemi merged commit bb6ea9b into main May 14, 2026
1 check failed
@ilmoniemi ilmoniemi deleted the feature/96 branch May 14, 2026 06:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(data): add unarchive() primitive to ConversationRepository + Fake impl

1 participant